package com.xuecheng.media.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xuecheng.base.exception.XueChengPlusException;
import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.base.utils.FFMpegUtils;
import com.xuecheng.content.checkutils.CheckInsertResultUtil;
import com.xuecheng.content.mapper.CourseBaseMapper;
import com.xuecheng.content.model.dto.*;
import com.xuecheng.content.model.po.MediaProcessHistory;
import com.xuecheng.media.client.CourseBaseInfoServiceClient;
import com.xuecheng.media.client.CourseTeacherServiceClient;
import com.xuecheng.media.config.MinioConfig;
import com.xuecheng.media.config.QueryWrapperBuilder;
import com.xuecheng.media.conver.MediaFilesConver;
import com.xuecheng.media.mapper.MediaFilesMapper;
import com.xuecheng.content.model.po.MediaFiles;
import com.xuecheng.media.service.MediaFilesService;
import com.xuecheng.media.service.MediaProcessHistoryService;
import io.minio.*;
import io.minio.errors.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.PostConstruct;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * <p>
 * 媒资信息 服务实现类
 * </p>
 *
 * @author Mia
 */
@Slf4j
@Service
public class MediaFilesServiceImpl extends ServiceImpl<MediaFilesMapper, MediaFiles> implements MediaFilesService {

    @Autowired
    private MediaFilesMapper mediaFilesMapper;

    @Autowired
    private MinioConfig minioConfig;

    @Autowired
    private MinioClient minioClient;

    @Value("${minio.bucket.files}")
    private String BUCKET_FILES;

    @Value("${minio.bucket.videofiles}")
    private String BUCKET_VIDEOS;

    @Value("${minio.bucket.chunkSizeMB}")
    private  int CHUNK_SIZE_MB;

    @Value("${minio.serviceAddress}")
    private String SERVICE_ADDRESS;

    @Value("${minio.bucket.uploadFileSize}")
    private String UPLOAD_FILE_SIZE;

    @Value("${minio.bucket.uploadFileChunkSize}")
    private String UPLOAD_FILE_CHUNK_SIZE;
    @Autowired
    private MediaProcessHistoryService mediaProcessHistoryService;

    private int CHUNK_SIZE;

    @PostConstruct
    public void init() {
        // 1. 解析配置
        Map<Long, Integer> sizeMap = parseSizeConfig();

        // 2. 设置默认分块大小 (单位：字节)
        CHUNK_SIZE = sizeMap.values().iterator().next() * 1024 * 1024;
    }

    @Autowired
    CourseBaseInfoServiceClient courseBaseInfoServiceClient;

    @Autowired
    CourseTeacherServiceClient courseTeacherServiceClient;


    @Autowired
    private CourseBaseMapper courseBaseMapper;

    // 支持的视频扩展名
    private static final List<String> VIDEO_EXTENSIONS = Arrays.asList(
            "mp4", "avi", "mov", "mkv", "wmv", "flv", "mpeg", "mpg", "ts", "webm", "3gp", "3g2"
    );

    /**
     * 初始化文件信息，包括MD5、扩展名、内容类型和存储桶
     *
     * @param file 上传的MultipartFile文件
     * @return MultPartFileInfoInit 包含文件基础信息的对象
     */
    public MultPartFileInfoInit multipartFileInfoInit(MultipartFile file) {
        MultPartFileInfoInit fileInfoInit = new MultPartFileInfoInit();
        try (InputStream is = file.getInputStream()) {
            // 一次性计算MD5并获取扩展名
            String md5 = DigestUtils.md5DigestAsHex(is);
            String ext = StringUtils.getFilenameExtension(file.getOriginalFilename());

            fileInfoInit.setFileName(file.getOriginalFilename());
            fileInfoInit.setFileMD5(md5);
            fileInfoInit.setExtension(ext != null ? ext.toLowerCase() : "");
            fileInfoInit.setFileType(determineContentType(ext));
            fileInfoInit.setBucketName(shouldUseVideoBucket(ext) ? BUCKET_VIDEOS : BUCKET_FILES);
//            fileInfoInit.setChunkSize(CHUNK_SIZE);
            fileInfoInit.setChunkSize(getChunkSize(file.getSize()));
            fileInfoInit.setFileSize(file.getSize());
        }catch(Exception e){
            throw new XueChengPlusException("文件初始化失败");
        }
        return fileInfoInit;
    }



    // 根据文件大小获取分块大小 (单位：字节)
    public int getChunkSize(long fileSizeBytes) {
        long fileSizeMB = fileSizeBytes / (1024 * 1024);
        Map<Long, Integer> sizeMap = parseSizeConfig();

        // 文件小于5MB，直接使用文件大小
        if (fileSizeMB < 5) {
            return (int) fileSizeBytes;
        }

        // 查找匹配的分块大小配置
        for (Map.Entry<Long, Integer> entry : sizeMap.entrySet()) {
            if (fileSizeMB <= entry.getKey()) {
                return entry.getValue() * 1024 * 1024;
            }
        }

        // 未找到匹配，返回最大的分块大小
        return sizeMap.values().stream()
                .max(Comparator.naturalOrder())
                .orElse(10) * 1024 * 1024;
    }

    // 解析配置文件格式: "100-500-1000-10000" 和 "5-10-20-40"
    private Map<Long, Integer> parseSizeConfig() {
        String[] sizes = UPLOAD_FILE_SIZE.split("-");
        String[] chunks = UPLOAD_FILE_CHUNK_SIZE.split("-");

        Map<Long, Integer> map = new LinkedHashMap<>();
        for (int i = 0; i < sizes.length; i++) {
            long size = Long.parseLong(sizes[i]);
            int chunk = Integer.parseInt(chunks[i]);
            map.put(size, chunk);
        }
        return map;
    }

    /**
     * 将文件分割为多个块并上传到MinIO
     *
     * @param file 上传的MultipartFile文件
     * @return MultPartFileInfoInit 包含所有分块路径的文件信息对象
     * @throws IOException 文件读取异常或输入流错误时抛出
     */
    public MultPartFileInfoInit splitFileToChunks(MultipartFile file) throws IOException {
        MultPartFileInfoInit fileInfo = multipartFileInfoInit(file);
        fileInfo.setChunkPaths(new ArrayList<>()); // 初始化列表

        try (InputStream fileStream = file.getInputStream()) {
            byte[] buffer = new byte[Math.toIntExact(fileInfo.getChunkSize())];
            int bytesRead;
            int chunkIndex = 0;

            while ((bytesRead = fileStream.read(buffer)) != -1) {
                byte[] chunkData = Arrays.copyOf(buffer, bytesRead);
                String chunkPath = uploadChunk(fileInfo, chunkIndex, chunkData);
                fileInfo.getChunkPaths().add(chunkPath);
                chunkIndex++;
            }
        } catch (MinioException e) {
            throw new RuntimeException(e);
        }
        return fileInfo;
    }

    /**
     * 上传单个文件块到MinIO存储 分块
     *
     * @param fileInfo 包含存储桶和内容类型信息的对象
     * @param chunkIndex 当前分块索引（从0开始）
     * @param chunkData 文件块二进制数据
     * @return String 上传成功的完整路径（格式：bucketName/objectPath）
     * @throws IOException 文件流操作异常时抛出
     * @throws MinioException MinIO客户端操作异常时抛出
     */
    private String uploadChunk(MultPartFileInfoInit fileInfo, int chunkIndex, byte[] chunkData)
            throws IOException, MinioException {

        String objectPath = generateChunkPath(fileInfo.getFileMD5(), chunkIndex);

        try (ByteArrayInputStream chunkStream = new ByteArrayInputStream(chunkData)) {
            minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(fileInfo.getBucketName())
                            .object(objectPath)
                            .stream(chunkStream, chunkData.length, -1)
                            .contentType(fileInfo.getFileType())
                            .build()
            );
            return fileInfo.getBucketName() + "/" + objectPath;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 上传图片到MinIO存储
     *
     * @param file 图片
     * @throws IOException 文件流操作异常时抛出
     * @throws MinioException MinIO客户端操作异常时抛出
     */
    private MultPartFileInfoInit uploadImages(MultipartFile file) throws IOException, MinioException {
        MultPartFileInfoInit fileInfo = multipartFileInfoInit(file);
        String objectPath = generateFinalFilePath(fileInfo.getFileMD5(),fileInfo.getExtension());

        try (InputStream inputStream = file.getInputStream()) {
            minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(fileInfo.getBucketName() )
                            .object(objectPath)
                            .stream(inputStream, file.getSize(), -1)
                            .contentType(fileInfo.getFileType())
                            .build()
            );

            // 4. 返回文件在MinIO中的存储路径（格式: bucket/objectPath）
            return fileInfo;
        } catch (Exception e) {
            throw new MinioException("上传图片到MinIO失败: " + e);
        }
    }

    /**
     * 根据文件扩展名确定内容类型
     *
     * @param extension 文件扩展名（不含点号）
     * @return String MIME内容类型字符串：
     *         视频文件 -> "video/bytes"
     *         其他文件 -> "application/octet-stream"
     *         空/无效扩展名 -> "application/octet-stream"
     */
    private String determineContentType(String extension) {

        if (extension == null || extension.trim().isEmpty()) {
            return "application/octet-stream";
        }
        String cleanExt = extension.trim().toLowerCase();        // 规范化扩展名（去掉首尾空格）
        // 核心检测：是否在视频扩展名列表中？
        return VIDEO_EXTENSIONS.contains(cleanExt) ? "video/bytes" : "application/octet-stream";
    }

    /**
     * 根据文件扩展名确定是否使用视频存储桶
     *
     * @param extension 文件扩展名（不含点号）
     * @return boolean 是否使用视频存储桶（VIDEO_EXTENSIONS列表中的文件返回true）
     */
    private boolean shouldUseVideoBucket(String extension) {
        return extension != null && VIDEO_EXTENSIONS.contains(extension.toLowerCase());
    }

    /**
     * 生成MinIO分块存储路径（基于文件MD5和分块索引）
     *
     * @param fileMd5 文件的MD5哈希值
     * @param chunkIndex 当前分块索引（从0开始）
     * @return String 分块存储路径（格式：<一级目录>/<二级目录>/<文件MD5>/chunk_<索引>）
     *         路径示例: "0/4d/04d6dcf6.../chunk_0"
     */
    public static String generateChunkPath(String fileMd5, int chunkIndex) {
        return String.format("%s/%s/%s/chunk_%d",fileMd5.substring(0, 1),fileMd5.substring(1, 3),fileMd5,chunkIndex);
    }

    /**
     * 生成合并后的最终文件路径
     *
     * @param md5 文件MD5值
     * @param extension 文件扩展名
     * @return String 最终文件存储路径
     */
    private String generateFinalFilePath(String md5, String extension) {
        return String.format("%s/%s/%s.%s",
                md5.substring(0, 1),
                md5.substring(1, 3),
                md5,
                (extension == null || extension.isEmpty()) ? "bin" : extension
        );
    }
    /**
     * 清理已合并的分块文件
     *
     * @param fileInfo 包含分块路径的文件信息
     */
    private void cleanupChunks(MultPartFileInfoInit fileInfo) {
        for (String chunkPath : fileInfo.getChunkPaths()) {
            // 提取对象路径（去掉桶名前缀）
            String objectPath = chunkPath.substring(fileInfo.getBucketName().length() + 1);

            try {
                minioClient.removeObject(
                        RemoveObjectArgs.builder()
                                .bucket(fileInfo.getBucketName())
                                .object(objectPath)
                                .build()
                );
            } catch (Exception e) {
                log.error("清理分块失败: {}", objectPath, e);
            }
        }
        log.info("已清理 {} 个分块文件", fileInfo.getChunkPaths().size());
    }

    /**
     * 合并MinIO中的分块文件
     *
     * @param fileInfo 包含文件信息和分块路径的对象
//     * @return String 合并后的文件完整路径
     */
    public void mergeChunks(MultPartFileInfoInit fileInfo) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        String finalObjectName = generateFinalFilePath(fileInfo.getFileMD5(),fileInfo.getExtension());
        System.out.println("finalObjectName"+finalObjectName);
        // 2. 准备分块源对象列表
        List<ComposeSource> sources = new ArrayList<>();
        for (String chunkPath : fileInfo.getChunkPaths()) {
            //    分块路径格式为 "bucket/object/path"    提取对象路径（去掉桶名前缀）
            String objectPath = chunkPath.substring(fileInfo.getBucketName().length() + 1);
            sources.add(ComposeSource.builder().bucket(fileInfo.getBucketName()).object(objectPath).build());
        }
        // 3. 执行合并操作
        minioClient.composeObject(ComposeObjectArgs.builder().bucket(fileInfo.getBucketName()).object(finalObjectName).sources(sources).build());
        // 4. 清理临时分块
        cleanupChunks(fileInfo);

//        return fileInfo.getBucketName() + "/" + finalObjectName;
    }

    @Override
    public MediaFilesUploadResultDto uploadFilesCourseBase(MultipartFile file, Long courseId) {
        MediaFilesUploadResultDto mediaFilesUploadResultDto = new MediaFilesUploadResultDto();      //封装结果
        MultPartFileInfoInit fileInfo = new MultPartFileInfoInit();         //文件基本信息
        MediaFiles mediaFileInsert = new MediaFiles();                      //数据库插入
        try{
//            fileInfo = splitFileToChunks(file);         //分块上传Minio
//            mergeChunks(fileInfo);                      //合并分块 合并后自动清理
            fileInfo = uploadImages(file);

//            拷贝类
            mediaFileInsert = convertedToMediaFile(fileInfo);
            CheckInsertResultUtil.checkExist(mediaFilesMapper.insert(mediaFileInsert),()-> new XueChengPlusException("文件资源更新失败"));
//            mediaFileInsert.setFilePath(generateFinalFilePath(fileInfo.getFileMD5(), fileInfo.getExtension()));
//            Optional.ofNullable(fileInfo.getBucketName()).map(bucketName ->BUCKET_FILES.equals(bucketName) ? "资源文件" : BUCKET_VIDEOS.equals(bucketName) ? "课程视频" : null).filter(obj -> true).ifPresent(mediaFileInsert::setTags);
//            MediaFilesConver.multPartFileInfoInitConverToMediaFiles(fileInfo, mediaFileInsert);
//
//            mediaFilesMapper.insert(mediaFileInsert);       //数据库记录

            String pic = SERVICE_ADDRESS +mediaFileInsert.getUrl();
            courseBaseInfoServiceClient.updateCoursePicture(courseId,pic);

//            构建返回类
            BeanUtils.copyProperties(mediaFileInsert,mediaFilesUploadResultDto);
            mediaFilesUploadResultDto.setBucketName(mediaFileInsert.getBucket());
            mediaFilesUploadResultDto.setUrl(SERVICE_ADDRESS+mediaFileInsert.getUrl());


        }catch (Exception e){
            e.printStackTrace();
            throw new XueChengPlusException("上传失败");
        }
        return mediaFilesUploadResultDto;
    }

    //MultPartFileInfoInit 转换为 MediaFiles
    @Override
    public MediaFiles convertedToMediaFile(MultPartFileInfoInit fileInfo){
        MediaFiles mediaFileInsert = new MediaFiles();                      //数据库插入
        mediaFileInsert.setFilePath(generateFinalFilePath(fileInfo.getFileMD5(), fileInfo.getExtension()));
        Optional.ofNullable(fileInfo.getBucketName()).map(bucketName ->BUCKET_FILES.equals(bucketName) ? "资源文件" : BUCKET_VIDEOS.equals(bucketName) ? "课程视频" : null)
                .filter(obj -> true).ifPresent(mediaFileInsert::setTags);
        MediaFilesConver.multPartFileInfoInitConverToMediaFiles(fileInfo, mediaFileInsert);

        return mediaFileInsert;       //数据库记录
    }

    @Override
    public PageResult<MediaFiles> queryMediaFiels(Long companyId, PageParams pageParams, QueryMediaParamsDto queryMediaParamsDto) {
        LambdaQueryWrapper<MediaFiles> wrapper = new LambdaQueryWrapper<>();
        new QueryWrapperBuilder<MediaFiles, QueryMediaParamsDto>(wrapper, queryMediaParamsDto)
                .addLikeCondition(queryMediaParamsDto::getFilename, MediaFiles::getFilename).addEqCondition(queryMediaParamsDto::getFileType, MediaFiles::getFileType);
        Page<MediaFiles> page = new Page<>(pageParams.getPageNo(),pageParams.getPageSize());
        Page<MediaFiles> mediaFilesPage = mediaFilesMapper.selectPage(page, wrapper);
        return new PageResult<>(mediaFilesPage.getRecords(),mediaFilesPage.getTotal(), pageParams.getPageNo(),pageParams.getPageSize());
    }

//     删除旧图片
    @Override
    public Boolean deleteUploadImageCourseBase(Long courseId) {
        Map<String, String> imageMap = courseBaseInfoServiceClient.CourseIdExtractMd5(courseId);
//        System.out.println("要删除图片id"+md5);
        String md5 = imageMap.get("md5");
        String url = imageMap.get("url");
        Optional.ofNullable(md5).ifPresent(item ->{mediaFilesMapper.deleteById(md5);});
        Optional.ofNullable(url).ifPresent(item ->{deleteFileByUrl(url);});
        return true;
    }

    @Override
    public void deleteMinioById(String fileId) {
        MediaFiles selectMediaFiles = mediaFilesMapper.selectOne(new LambdaQueryWrapper<MediaFiles>().eq(MediaFiles::getFileId,fileId));
        Optional.ofNullable(selectMediaFiles.getUrl()).ifPresent(item -> {
            String[] bucketAndObject = parseMediaFileUrl(selectMediaFiles.getUrl());
            if (bucketAndObject == null || bucketAndObject.length != 2) {
                throw new XueChengPlusException("文件解析失败");
            }
            deleteFileByBucketAndObject(bucketAndObject[0],bucketAndObject[1]);
            int deletedCount = mediaFilesMapper.delete(new LambdaQueryWrapper<MediaFiles>().eq(MediaFiles::getFileId, fileId));
        });
    }

    @Override
    public Boolean deleteUploadImageCourseTeacher(Long teacherId) {
        Map<String, String> imageMap = courseTeacherServiceClient.courseTeacherExtractMd5(teacherId);
        String md5 = imageMap.get("md5");
        String url = imageMap.get("url");
        Optional.ofNullable(md5).ifPresent(item ->{mediaFilesMapper.deleteById(md5);});     //删除文件表对应记录
        Optional.ofNullable(url).ifPresent(item ->{deleteFileByUrl(url);});                 //通过url 删除minio上的文件
        return true;
    }

    @Override
    public MediaFilesUploadResultDto uploadFilesCourseTeacher(MultipartFile file, Long teacherId) {
        MediaFilesUploadResultDto mediaFilesUploadResultDto = new MediaFilesUploadResultDto();      //封装结果
        MultPartFileInfoInit fileInfo = new MultPartFileInfoInit();         //文件基本信息
        MediaFiles mediaFileInsert = new MediaFiles();                      //数据库插入
        try{
            fileInfo = uploadImages(file);      //上传文件

//            拷贝类
            mediaFileInsert.setFilePath(generateFinalFilePath(fileInfo.getFileMD5(), fileInfo.getExtension()));
            Optional.ofNullable(fileInfo.getBucketName()).map(bucketName ->BUCKET_FILES.equals(bucketName) ? "资源文件" : BUCKET_VIDEOS.equals(bucketName) ? "课程视频" : null).filter(obj -> true).ifPresent(mediaFileInsert::setTags);
            MediaFilesConver.multPartFileInfoInitConverToMediaFiles(fileInfo, mediaFileInsert);
            mediaFilesMapper.insert(mediaFileInsert);       //数据库记录

            String pic = SERVICE_ADDRESS +mediaFileInsert.getUrl();
            courseTeacherServiceClient.updateCourseTeacherPicture(teacherId,pic);

//            构建返回类
            BeanUtils.copyProperties(mediaFileInsert,mediaFilesUploadResultDto);
            mediaFilesUploadResultDto.setBucketName(mediaFileInsert.getBucket());
            mediaFilesUploadResultDto.setUrl(SERVICE_ADDRESS+mediaFileInsert.getUrl());


        }catch (Exception e){
            e.printStackTrace();
            throw new XueChengPlusException("上传失败");
        }
        return mediaFilesUploadResultDto;
    }

//    更新课程计划 资源文件
    @Override
    public TeachplanAndTMediaAndMediaFileDto uploadCourseOutlineNode(MultipartFile file, Long teacherId) {
        MultPartFileInfoInit fileInfo = new MultPartFileInfoInit();         //文件信息初始化
        MediaFiles mediaFileInsert = new MediaFiles();                      //数据库插入

        try{
            fileInfo = splitFileToChunks(file);//分块上传Minio
            mergeChunks(fileInfo);                      //合并分块 合并后自动清理
        }catch(Exception e){
            e.printStackTrace();
        }
//        mediaFiles进行新增
        mediaFileInsert = convertedToMediaFile(fileInfo);
        CheckInsertResultUtil.checkExist(mediaFilesMapper.insert(mediaFileInsert),()-> new XueChengPlusException("文件资源更新失败"));
//        mediaFileInsert.setFilePath(generateFinalFilePath(fileInfo.getFileMD5(), fileInfo.getExtension()));
//        Optional.ofNullable(fileInfo.getBucketName()).map(bucketName ->BUCKET_FILES.equals(bucketName) ? "2" : BUCKET_VIDEOS.equals(bucketName) ? "1" : null).filter(obj -> true).ifPresent(mediaFileInsert::setTags);
//        MediaFilesConver.multPartFileInfoInitConverToMediaFiles(fileInfo, mediaFileInsert);
//        mediaFilesMapper.insert(mediaFileInsert);       //数据库记录

        MediaFilesDto mediaFilesDto = new MediaFilesDto();
        BeanUtils.copyProperties(mediaFileInsert,mediaFilesDto);
        mediaFilesDto.setTeacherId(teacherId);
        Optional.ofNullable(mediaFileInsert.getFileType())
                .filter("001002"::equals)
                .ifPresent(item -> {
                    try {
                        double videoDuration = FFMpegUtils.getVideoDuration(file);
                        mediaFilesDto.setTimelength(String.valueOf(videoDuration));
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                });

        TeachplanAndTeachplanMediaDto teachplanAndTeachplanMediaDto = courseTeacherServiceClient.teachplanMediaUpdateFile(mediaFilesDto);
        Optional.ofNullable(teachplanAndTeachplanMediaDto.getOldTeachplanMediaDto()).ifPresent(item ->{
            deleteMinioById(item.getMediaId());
        });

        return convertToDto(teachplanAndTeachplanMediaDto);
    }

    @Override
    public int insert(MediaFiles mediaFilesNew) {
        return mediaFilesMapper.insert(mediaFilesNew);
    }

    @Override
    public int updateNotEmpty(MediaFiles mediaFiles) {
        return mediaFilesMapper.updateNotEmpty(mediaFiles);
    }

    @Override
    public int updateNotEmptyByFileId(MediaFiles mediaFiles) {
        return mediaFilesMapper.updateNotEmptyByFileId(mediaFiles);
    }

    @Override
    public String selectUrlById(Long mediaId) {
        MediaFiles mediaFiles = mediaFilesMapper.selectById(mediaId);
        if(mediaFiles != null){
            return "/"+mediaFiles.getUrl();
        }
        return null;
    }

    //    构建出课程计划的完整返回信息
    public TeachplanAndTMediaAndMediaFileDto convertToDto(TeachplanAndTeachplanMediaDto sourceDto) {
        // 1. 创建目标对象并复制基础属性
        TeachplanAndTMediaAndMediaFileDto targetDto = new TeachplanAndTMediaAndMediaFileDto();
        BeanUtils.copyProperties(sourceDto, targetDto);
        targetDto.setTeachplanMediaDto(sourceDto.getNewTeachplanMediaDto());
        return targetDto;
    }

    /**
     * 根据桶名和对象名删除MinIO文件
     *
     * @param bucketName MinIO存储桶名称
     * @param objectName 对象路径
     * @return 是否删除成功
     */
    public boolean deleteFileByBucketAndObject(String bucketName, String objectName) {
        try {
            // 执行删除操作
            minioClient.removeObject(
                    RemoveObjectArgs.builder()
                            .bucket(bucketName)
                            .object(objectName)
                            .build());

            return true;
        } catch (MinioException | IOException | java.security.InvalidKeyException e) {
            System.err.println("文件删除失败: " + e.getMessage());
            return false;
        } catch (NoSuchAlgorithmException e) {
            System.err.println("加密算法不存在: " + e.getMessage());
            throw new RuntimeException(e);
        }
    }

    /**
     * 通过完整URL删除MinIO文件
     *
     * @param fullFileUrl 完整的MinIO文件URL
     * @return 是否删除成功
     */
    public boolean deleteFileByUrl(String fullFileUrl) {
        // 1. 解析URL获取存储桶和对象名称
        String[] bucketAndObject = parseUrl(SERVICE_ADDRESS, fullFileUrl);

        if (bucketAndObject == null || bucketAndObject.length != 2) {
            System.err.println("URL解析失败: " + fullFileUrl);
            return false;
        }

        String bucketName = bucketAndObject[0];
        String objectName = bucketAndObject[1];

        try {
            //  执行删除操作
            minioClient.removeObject(
                    RemoveObjectArgs.builder()
                            .bucket(bucketName)
                            .object(objectName)
                            .build());

            return true;
        } catch (MinioException | IOException | java.security.InvalidKeyException e) {
            return false;
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 解析MinIO URL获取存储桶和对象名
     *
     * 示例URL: http://192.168.101.65:9000/mediafiles/2/e5/2e58523941a73242a1389886c29f2585.png
     *
     * @param baseEndpoint MinIO服务端点 (如: http://192.168.101.65:9000)
     * @param fullUrl 完整文件URL
     * @return [存储桶名称, 对象名称] 或 null
     */
    private String[] parseUrl(String baseEndpoint, String fullUrl) {
        // 移除MinIO服务器地址部分
        if (!fullUrl.startsWith(baseEndpoint)) {
            System.err.println("URL不匹配: " + fullUrl);
            return null;
        }

        // 获取存储桶和对象路径部分
        String bucketAndObject = fullUrl.substring(baseEndpoint.length() ); // 跳过斜杠

        // 查找存储桶名称结束位置（第一个斜杠之前）
        return parseMediaFileUrl(bucketAndObject);
    }

//    通过数据库文件url 解析出 bucket 和 文件路径
    public String[] parseMediaFileUrl(String bucketAndObject){
        int firstSlash = bucketAndObject.indexOf('/');
        if (firstSlash <= 0) {
            System.err.println("URL格式无效: " + bucketAndObject);
            return null;
        }

        // 分离存储桶名和对象名
        String bucketName = bucketAndObject.substring(0, firstSlash);
        String objectName = bucketAndObject.substring(firstSlash + 1);

        return new String[]{bucketName, objectName};
    }



    //    检测文件是否存在
    public Boolean checkFile(String fileMd5) {
        MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);       // 查询出对应文件记录
//        查询
        try {
            StatObjectResponse response = minioClient.statObject(StatObjectArgs.builder().bucket(mediaFiles.getBucket()).object(mediaFiles.getFilePath()).build());
            return true;
        } catch (ErrorResponseException e) {
            // 只处理"文件不存在"这种特定错误
            if ("NoSuchKey".equals(e.errorResponse().code())) {
                return false;
            }
            throw new XueChengPlusException("MinIO错误: ");
        } catch (Exception e) {
            // 处理其他未知异常
            throw new XueChengPlusException("文件检测失败");
        }
    }

    //    加入代办处理任务
    public List<MediaFilesTaskDto> selectWaitConvertedVideos() {
        // 1. 获取所有符合条件的媒体文件（使用 Set 避免重复）
        Set<String> fileIdSet = new HashSet<>();
        List<MediaFilesTaskDto> results = new ArrayList<>();
        List<MediaFiles> mediaFilesList = new ArrayList<>();


        // 2. 添加原始的非MP4文件
        List<MediaFiles> firstFiles = mediaFilesMapper.selectList(new LambdaQueryWrapper<MediaFiles>().eq(MediaFiles::getFileType, "001002")
                .eq(MediaFiles::getAuditStatus, "202003"));

        firstFiles.stream().filter(mediaFile -> {
                    String suffix = fileSuffixExtractor(mediaFile.getUrl());
                    return suffix != null && !suffix.equalsIgnoreCase("mp4");
                }).filter(mediaFile -> fileIdSet.add(mediaFile.getFileId())) // 确保唯一性
                .forEach(mediaFilesList::add);



        // 3. 添加失败记录（排除已存在和MP4格式文件）
        List<MediaProcessHistory> failMediaProcessHistory = mediaProcessHistoryService.selectFailFileID();
        for(MediaProcessHistory mediaProcessHistory : failMediaProcessHistory){
            if(mediaProcessHistory != null || mediaProcessHistory.getFileId() != null){
                MediaFiles selectMediaFiles = mediaFilesMapper.selectOne(new LambdaQueryWrapper<MediaFiles>().eq(MediaFiles::getFileId, mediaProcessHistory.getFileId()));
                MediaFilesTaskDto mediaFilesTaskDto = new MediaFilesTaskDto();
                BeanUtils.copyProperties(selectMediaFiles,mediaFilesTaskDto);
                Optional.ofNullable(mediaProcessHistory.getFailCount()).ifPresent(mediaFilesTaskDto::setFailCount); //接受次数
                mediaFilesTaskDto.setMediaProcessHistoryId(mediaProcessHistory.getId());
                results.add(mediaFilesTaskDto); //加入列表
            }
        }

        // 1. 将原始文件转换为TaskDto对象
        Map<String, MediaFilesTaskDto> mergedMap = mediaFilesList.stream()
                .map(mediaFile -> {
                    MediaFilesTaskDto dto = new MediaFilesTaskDto();
                    BeanUtils.copyProperties(mediaFile, dto);
                    return dto;
                })
                .collect(Collectors.toMap(MediaFilesTaskDto::getFileId, dto -> dto));

        // 2. 合并失败记录（覆盖原始记录）
        for (MediaFilesTaskDto failedDto : results) {
            mergedMap.put(failedDto.getFileId(), failedDto);            // 始终优先保留失败记录
        }

        // 3. 返回合并后的结果集
        return new ArrayList<>(mergedMap.values());
    }


//    public List<MediaFiles> selectWaitConvertedVideos(){
//        // 1. 获取所有符合条件的媒体文件（使用 Set 避免重复）
//        Set<String> fileIdSet = new HashSet<>();
//        List<MediaFiles> results = new ArrayList<>();
//
//        List<MediaFiles> selectMediaFiles = mediaFilesMapper.selectList(new LambdaQueryWrapper<MediaFiles>().eq(MediaFiles::getFileType, "001002").eq(MediaFiles::getAuditStatus,"202003"));  // 查询对应的记录
//
//        List<MediaFiles> selectMediaFilesPending = selectMediaFiles.stream().filter(mediaFile -> {
//            String suffix = fileSuffixExtractor(mediaFile.getUrl());
//            return suffix != null && !suffix.equalsIgnoreCase("mp4");
//        }).collect(Collectors.toList());
//
////        查询失败记录
//        List<String> failList = mediaProcessHistoryService.selectFailFileID();
//        failList.stream().forEach(fileId ->{
//            MediaFiles mediaFiles = mediaFilesMapper.selectById(fileId);
//            Optional.ofNullable(mediaFiles).ifPresent(item ->{
//                selectMediaFilesPending.add(mediaFiles);
//            });
//        });
//
//        return selectMediaFilesPending;
//    }

//    提取文件后缀
    public String fileSuffixExtractor(String url){
        String regex = ".*/([^/]+)\\.([^.]+)$";
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(url);
        return matcher.find() && matcher.groupCount() == 2 ? matcher.group(2) : null;
    }
}
